/**
 *
 * @kd-submodules 组件一键发布脚本 deploy.js
 *
 * =================================================================================================================
 *
 * 作用：由于案事件组件之间相互依赖，导致一个组件的版本修改会牵连到其他相关组件一起修改，人肉搜索各种依赖、修改版本号异常艰难。
 *      deploy.js 可以帮助使用者自动完成组件版本号升级过程。
 *
 * 使用：1.将 deploy.js 文件拷贝至某一路径下
 *      2.在该文件夹下启动命令行
 *      3.命令行输入 node deploy.js -h 查看具体使用方式
 *
 * 局限：deploy.js 只能帮助开发者修改组件的 package.json 中的版本号，并不能帮助开发者完成 git add、git commit 这类的提交操作
 *      所有修改还是需要人为进行提交。
 *
 * 注意，在使用 deploy.js 升级组件版本之前：
 *      1. 请保证本地仓库是最新代码、仓库无修改记录，否则可能会产生冲突
 *      2. 请确认当前组件的分支，是否位于正确的分支上
 *      3. 请保证本地的组件文件夹命名，和 gitlab 上 kd-submodules 下的组件同名
 *
 * =================================================================================================================
 *
 */

const fs = require('fs');
const path = require('path');

// 归我们处理的包名列表
// 如果有新封装的包，需要将对应的报名写入变量
// 包名不需要加上 "@kd-submodules/"
// 请保证本地组件文件夹的命名和实际组件名一致
const PACKAGE_NAME_LIST = [
    'caseBase',
    'caseManagement',
    'caseClue',
    'caseFlow',
    'caseStatistics',
    'clueDetail',
    'caseCategoryDialog',
    'caseNavBall',
    'regionDialog',
    'clueCategoryDialog'
];

// console.log 颜色映射
const COLOR_MAP = {
    red: '\x1B[31m%s\x1B[39m',
    green: '\x1B[32m%s\x1B[39m',
    yellow: '\x1B[33m%s\x1B[39m'
};


const args = process.argv.splice(2); // 获取终端里输入的自定义参数
let helpMode = false; // 是否是帮助模式，如果是帮助模式，就不执行程序，只打印使用方法
let autoDeploy = false; // 依赖当前包的包是否也要自动发布到对应版本
let rootPath = __dirname; // 脚本执行在哪个根路径
let packageNames = []; // 需要更新的包名列表
let packageNames_copy = []; // 原始 packageNames 的备份值
let packageVersion = ''; // 需要更新到的版本号
let space = 2; // 默认 JSON 缩进
let changedFileList = []; // 被改动的文件列表
let depPackageNames = []; // 依赖当前包的包名列表
let relativePackages = []; // 所有和当前包升级相关的包名列表（包括i依赖包和当前包）

// 分析终端输入的参数
analyseArgs(args);

// 在非帮助模式下，程序才会执行
!helpMode && run(packageNames, autoDeploy);



/**
 * 解析终端传入的参数
 * @param {*} args 终端传入的参数
 */
function analyseArgs (args) {
    const funcMap = {
        // 帮助
        '-h': (args, index) => {
            helpMode = true;
    console.log(`
    1. '-p' 参数指定执行路径 (必传)\n
    2. '-v' 参数指定发布版本 (必传)\n
    3. '-n' 参数指定组件名称，可同时更新多组件到统一版本，组件名之间使用空格分隔 (必传)\n
    4. '-h' 参数查看帮助\n
    5. '--auto' 参数指定自动发布模式（如果当前包的版本升级，那么所有依赖当前包的组件，都会升级到同一版本号）\n\n`);
    console.log(COLOR_MAP.yellow, `
    正确使用姿势：\n
    1. 查看帮助：node deploy.js -h\n
    2. 单组件手动更新：node deploy.js -n caseBase -v 1.0.0 -p E:/test/@kd-submodules\n
    3. 多组件手动更新：node deploy.js -n caseBase caseManagement caseClue -v 1.0.0 -p E:/test/@kd-submodules\n
    4. 单组件自动更新：node deploy.js -n caseBase -v 1.0.0 -p E:/test/@kd-submodules --auto\n
    5. 多组件自动更新：node deploy.js -n caseBase caseManagement regionDialog -v 1.0.0 -p E:/test/@kd-submodules --auto\n
    `);
        },
        // 路径
        '-p': (args, index) => rootPath = args[index + 1],
        // 版本
        '-v': (args, index) => packageVersion = args[index + 1],
        // 包名
        '-n': (args, index) => {
            const nextIndex = index + 1;
            // 从终端参数输入中生成 packageNames
            packageNames = (arr => {
                let res = [];
                for (let i = 0, item; i < arr.length; i++) {
                    item = arr[i];
                    if (item[0] === '-') break;
                    res.push(item);
                }
                return res.map(item => item.indexOf('@kd-submodules/') > -1 ? item : `@kd-submodules/${item}`);
            })(args.slice(nextIndex));
            packageNames_copy = [...packageNames];
        },
        // 自动化发布
        '--auto': (args, index) => autoDeploy = true
    };

    args.forEach((val, index, arr) => funcMap[val] && funcMap[val](arr, index));
}


/**
 * 执行脚本
 * @param {*} packageNames 包名
 */
function run (packageNames, autoDeploy = false) {
    let errorList = []; // 错误存放栈
    let warningList = []; // 警告存放栈

    packageNames.forEach(packageName => {
        let res = exec(packageName);

        // 如果有错，就推入错误列表
        res.error && errorList.push(res);

        // 如果是警告，就推入警告
        res.warning && warningList.push(res);

        if (autoDeploy) {
            relativePackages = [ ...relativePackages, ...depPackageNames ];
        } else {
            notice(res);
            changedFileList = [];
        }
        depPackageNames = [];
    });

    // 不是自动发布模式，下面的代码不执行
    if (!autoDeploy) return;

    // new Set 去重
    relativePackages = [...new Set([...relativePackages, ...packageNames_copy])];
    changedFileList = [...new Set(changedFileList)];

    // 如果 relativePackages 和 packageNames 列表中的数据一样了
    // 就跳出递归，打印结果
    // 否则进行递归操作
    if (isEqual(relativePackages, packageNames)) {
        let errorPackageNames = errorList.map(({ packageName }) => packageName);
        let warningPackageNames = warningList.map(({ packageName }) => packageName);
        // 成功发布的包名列表里不应该包含 warning 和 error 的包名
        let successPackageNames = relativePackages.filter(item => !errorPackageNames.includes(item) && !warningPackageNames.includes(item));

        let res = {
            success: successPackageNames,
            error: errorList,
            warning: warningList
         };
        notice(res);
    } else {
        run(relativePackages, autoDeploy);
    }
}


/**
 * 执行发布函数
 * @param {*} packageNames 包名
 */
function exec (packageName) {
    // 获取所有 package.json 文件的绝对路径
    let packageJSONFiles = getPackageJSONPath(rootPath);
    let err = null;

    // 如果没有找到 package.json 文件
    // 或者指定包名不再 PACKAGE_NAME_LIST 里面
    // 就输出一个 warning
    if (!packageJSONFiles.length || !PACKAGE_NAME_LIST.includes(packageName.replace('@kd-submodules/', ''))) {
        return { success: false, packageName, warning: true };
    }

    for (let i = 0, filePath; i < packageJSONFiles.length; i++) {
        filePath = packageJSONFiles[i];

        // 针对每个 package.json 做单独发布处理
        err = deploy(filePath, packageName);
        if (err) {
            return { success: false, packageName, err };
        }
    }

    if (changedFileList.length) {
        return { success: true, packageName };
    } else {
        return { success: false, packageName };
    }
}



/**
 * 获取指定路径下所有 package.json 的全路径
 * @param {*} dirpath 脚本执行的根路径
 * @param {*} includes 包名列表
 */
function getPackageJSONPath (dirpath = __dirname, includes = PACKAGE_NAME_LIST) {
    let list = [];
    let nameSet = [];
    try {
        nameSet = fs.readdirSync(dirpath);
    } catch (e) {
        console.log(e);
    }

    // 如果在指定目录下找到了 package.json 就 push 到数组里
    if (nameSet.includes('package.json')) {
        list.push(path.resolve(dirpath, 'package.json'));
    }

	nameSet.forEach((name, i) => {
		const currentPath = path.resolve(dirpath, name);
        const isDir = fs.statSync(currentPath).isDirectory();
        const packageJSONPath = path.resolve(currentPath, 'package.json');

		if (isDir && includes.indexOf(name) > -1) {
            if (fs.existsSync(packageJSONPath)) list.push(packageJSONPath);
            list = [...list, ...getPackageJSONPath(currentPath, includes)];
		}
    });

	return [...new Set(list)];
}


/**
 * 发布
 * @param {*} filePath 需要操作的目标 package.json 文件绝对路径
 * @param {*} packageName 包名
 */
function deploy (filePath, packageName) {
    let jsonData = null;
    let packageInfo = {};

    try {
        jsonData = fs.readFileSync(filePath, 'utf8');
    } catch (e) {
        return e;
    }

    try {
        packageInfo = JSON.parse(jsonData);
    } catch (e) {
        return e;
    }

    let {
        name,
        peerDependencies
    } = packageInfo;

    // 如果是完全不相干的东西，就不浪费时间了
    if (name !== packageName && (!peerDependencies || !peerDependencies[packageName])) {
        return null;
    }

    // 如果当前模块就是目标模块
    // 则修改 version
    if (name === packageName) {
        packageInfo.version = packageVersion;
    } else if (peerDependencies && peerDependencies[packageName]) {
        // 否则查找 peerDependencies，如果有依赖此包，则修改版本
        peerDependencies[packageName] = packageVersion;
        depPackageNames.push(name);
    }

    let result = JSON.stringify(packageInfo, null, space);

    try {
        fs.writeFileSync(filePath, result, 'utf8');
    } catch (e) {
        return e;
    }

    // 将这个文件路径 push 到被修改的文件列表
    changedFileList.push(filePath);
}


/**
 * 控制台打印
 * @param {*} success 是否成功
 * @param {*} packageName 包名
 * @param {*} error error 对象
 */
function notice ({ success = false, packageName = '', warning = null, error = null }) {
    const noticeCbMap = {
        error: function () {
            if (Array.isArray(error)) {
                error.forEach(({ packageName, error }) => {
                    console.log(COLOR_MAP.red, ` \n "${packageName}" 包升级出错，错误信息：\n ${error}`);
                })
            } else {
                console.log(COLOR_MAP.red, ` \n "${packageName}" 包升级出错，错误信息：\n ${error}`);
            }
        },
        warning: function () {
            if (Array.isArray(warning)) {
                warning.forEach(({ packageName }) => {
                    console.log(COLOR_MAP.yellow, `\n 在指定目录 "${rootPath}" 下并未找到 "${packageName}" 相关组件`);
                })
            } else {
                console.log(COLOR_MAP.yellow, `\n 在指定目录 "${rootPath}" 下并未找到 "${packageName}" 相关组件`);
            }
        },
        success: function () {
            if (Array.isArray(success)) {
                console.log(COLOR_MAP.green, `\n 成功升级版本的组件为：\n ${success.join('\n ')}`);
                console.log(COLOR_MAP.green, `\n 与本次升级的相关组件为：\n ${relativePackages.join('\n ')}`);
                console.log(COLOR_MAP.green, `\n 改动的文件数量为：${changedFileList.length} 个`);
                console.log(COLOR_MAP.green, `\n 改动的文件路径为：\n ${changedFileList.join('\n ')}`);
            } else {
                console.log(COLOR_MAP.green, `\n "${packageName}" 包已升级为："${packageVersion}" \n`);
                console.log(COLOR_MAP.green, `\n 改动的文件数量为：${changedFileList.length} 个`);
                console.log(COLOR_MAP.green, `\n 改动的文件路径为：\n ${changedFileList.join('\n ')}`);
                console.log(COLOR_MAP.green, `\n 依赖此组件的相关组件为：\n ${depPackageNames.join('\n ')}`);
            }
        }
    };

    error && noticeCbMap.error();
    warning && noticeCbMap.warning();
    success && noticeCbMap.success();

    console.log('================================================================ \n');
}


/**
 * 判断两个对象是否一样（注意，一样不是相等）
 * 1. 如果是非引用类型的值，直接使用全等比较
 * 2. 如果是数组或对象，则会先比较引用指针是否一一致
 * 3. 引用指针不一致，再比较每一项是否相同
 *
 * @param target {All data types} 参照对象
 * @param obj {All data types} 比较对象
 * @returns {*}
 */
function isEqual(target, obj) {
    if (typeof target !== typeof obj) {
        return false;
    } else if (typeof target === 'object') {
        if (target === obj) { // 先比较引用
            return true;
        } else if (Array.isArray(target)) { // 数组
            if (target.length !== obj.length) { // 长度不同直接 return false
                return false;
            } else { // 否则依次比较每一项
                return target.every((item, i) => isEqual(item, obj[i]));
            }
        } else { // 对象
            const targetKeyList = Object.keys(target);
            const objKeyList = Object.keys(obj);
            if (targetKeyList.length !== objKeyList.length) { // 如果 keyList 的长度不同直接 return false
                return false;
            } else {
                return targetKeyList.every((key) => isEqual(target[key], obj[key]));
            }
        }
    } else {
        return target === obj;
    }
}
